/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */

package fr.inria.openmath.omapi.implementation;

/**
 *
 * @author Mr.Sam
 */

import java.io.*;
import java.lang.*;
import fr.inria.openmath.omapi.*;

public class UTF7Codec {

    // marker for encoded part start
    final static byte SHIFT_IN = '+';

    // marker for encoded part (explicit) end
    final static byte SHIFT_OUT = '-';

    // all these chars are output whithout encoding
    // theay are 'direct' chars
    final static String directList =
            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'(),./:?!\"#$%&*;=@[]^_`{|}"
            ;

    // base 64 digits (A->0, B->1, ..., /->63)
    final static char base64[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I',
            'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
            'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
            'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
            'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8',
            '9', '+', '/'};

    // given a base 64 digit char recover its base 10 value
    // -1 for non legal digits
    // inv64['A'] -> 0
    // inv64['/'] -> 63
    // inv64['?'] -> -1
    final byte inv64[];

    // for each char between 0 and 128 mark if it is a 'direct' char 
    // or a 'need shift' char
    // direct['a'] -> true
    // direct['+'] -> false
    final boolean direct[];

    boolean needShift = false;

    boolean shifted = false;

    long bitBuffer = 0;

    int bitCount = 0;

    public UTF7Codec () {
        int i;

        // init the fast access table for direct encoding chars
        direct = new boolean[128];
        for (i = 0; i < directList.length(); i++) {
            direct[(int) directList.charAt(i)] = true;

        }
        // init the fast access table for base 64 decoding
        inv64 = new byte[128];
        for (i = 0; i < 128; i++) {
            inv64[i] = -1;
        }
        for (i = 0; i < 64; i++) {
            inv64[(byte) base64[i]] = (byte) i;
        }
    }

    /*
                            private void printBitBuffer(int l) {
                                int i;
                                System.out.print("[" + l + ":");
                                for (i = l - 1; i >= 0; i--) {
                                    System.out.print((((1 << i) & bitBuffer) != 0) ? 1 : 0);
                                }
                                System.out.println("]");
                            }
                        
                            private void printBitChar(char c) {
                                int i;
                                System.out.print("[" + c + ":");
                                for (i = 15; i >= 0; i--) {
                                    System.out.print((((1 << i) & c) != 0) ? 1 : 0);
                                }
                                System.out.println("]");
                            }
                        */



    //
    // encoding methods
    //

    private int outputBits(OutputStream output, char c) throws IOException {
        int n;

        bitBuffer = (bitBuffer << 16) | c;
        bitCount += 16;
        if (bitCount >= 60) {
            // enough bits... output a bunch of base64
            while (bitCount > 5) {
                bitCount -= 6;
                n = (int) (bitBuffer & ((0x3fL) << bitCount));
                output.write(base64[n]);
                System.out.print((char) (base64[n]));
            }
            return 10;
        }
        return 0;
    }


    private int flushBits(OutputStream output) throws IOException {
        int l = 0, n, pad;

        if (shifted) {
            // padd to multiple of 6
            pad = 6 - (bitCount % 6);
            bitBuffer = bitBuffer << pad;
            bitCount += pad;
            // and flush that
            while (bitCount > 0) {
                bitCount -= 6;
                n = (int) ((bitBuffer & ((0x3fL) << bitCount)) >>> bitCount);
                output.write(base64[n]);
                l++;
            }
            l++;
            return l;
        } else {
            return 0;
        }
    }

    private int writeChar(OutputStream output, char c) throws IOException {
        int l = 0;

        needShift = ((c > 0x007f) || (!direct[(byte) c]));
        if (needShift && !shifted) {
            output.write(SHIFT_IN);
            // Special case handling of the SHIFT_IN character 
            if (c == (char) SHIFT_IN) {
                output.write(SHIFT_OUT);
                l += 2;
            } else {
                shifted = true;
                l++;
            }
        }

        if (shifted) {
            if (needShift) {
                // must output this char shifted
                l += outputBits(output, c);
            //printBitChar(c);
            //printBitBuffer();
            } else {
                // this is a direct char but we are in a shift context
                // thus flush buffer and close shift context
                // then output this direct char 
                l += 1 + flushBits(output);
                if (inv64[c] >= 0) {
                    // close 'shifted' context explicitly
                    // this char is in base 64 set of digits
                    // thus the implicit close can't be used
                    output.write(SHIFT_OUT);
                }
                output.write(c);
                shifted = false;
            }
            return l;
        } else {
            // direct char in non shifted context
            output.write(c);
            return 1;
        }
    }

    public int writeString(OutputStream output, String s) throws IOException {
        int i, l = 0;

        // we start another encoding (reset context)
        shifted = false;
        bitCount = 0;
        // encode all chars
        for (i = 0; i < s.length(); i++) {
            l += writeChar(output, s.charAt(i));
        }
        l += flushBits(output);
        return l;
    }

    //
    // decoding methods
    //

    private void inputBits(char c, StringBuffer sb) {
        bitBuffer = (bitBuffer << 6) | inv64[c];
        if (bitCount == 32) {
            // enough bits... decode 3 chars
            sb.append((char) ((bitBuffer & ((0xffffL) << 32)) >> 32));
            sb.append((char) ((bitBuffer & ((0xffffL) << 16)) >> 16));
            sb.append((char) (bitBuffer & (0xffffL)));
            bitCount = 0;
        } else {
            bitCount += 6;
        }
    }

    private void collectBits(StringBuffer sb) throws OMException {
        int realBitCount;
        // remove 0 pad bits
        realBitCount = (bitCount >> 4) << 4;
        bitBuffer = bitBuffer >> (bitCount - realBitCount);
        switch (realBitCount) {
            case 32: 
                sb.append((char) ((bitBuffer & ((0xffffL) << 16)) >> 16));
                sb.append((char) (bitBuffer & (0xffffL)));
                break;
            case 16: 
                sb.append((char) (bitBuffer & (0xffffL)));
                break;
        }
        shifted = false;
        bitCount = 0;
    }

    public String decodeString(String encoded) throws OMException {
        int l = encoded.length(), i;
        StringBuffer sb = new StringBuffer(l);
        char c;

        // System.out.println("decode utf7: " + encoded);
        shifted = false;
        for (i = 0; i < l; i++) {
            c = encoded.charAt(i);
            if (c == SHIFT_IN) {
                shifted = true;
            } else if (c == SHIFT_OUT) {
                // explicit shift out
                collectBits(sb);
                shifted = false;
            } else if (inv64[c] < 0) {
                // implicit shift out
                collectBits(sb);
                shifted = false;
                // direct char
                sb.append(c);
            } else if (shifted) {
                // in shift context... add these bits to buffer
                inputBits(c, sb);
            } else {
                // direct char
                sb.append(c);
            }
        }
        // collect remaining bits
        collectBits(sb);
        return new String(sb);
    }


}
